<?php
/**
 * CGettextMoFile class file.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
 * @copyright Copyright &copy; 2008-2011 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

/**
 * CGettextMoFile represents an MO Gettext message file.
 *
 * This class is written by adapting Michael's Gettext_MO class in PEAR.
 * Please refer to the following license terms.
 *
 * Copyright (c) 2004-2005, Michael Wallner <mike@iworks.at>.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id: CGettextMoFile.php 2798 2011-01-01 19:29:03Z qiang.xue $
 * @package system.i18n.gettext
 * @since 1.0
 */
class CGettextMoFile extends CGettextFile
{
    /**
     * @var boolean whether to use Big Endian when reading and writing an integer.
     */
    public $useBigEndian=false;

    /**
     * Constructor.
     * @param boolean $useBigEndian whether to use Big Endian when reading and writing an integer.
     */
    public function __construct($useBigEndian=false)
    {
        $this->useBigEndian=$useBigEndian;
    }

    /**
     * Loads messages from an MO file.
     * @param string $file file path
     * @param string $context message context
     * @return array message translations (source message => translated message)
     */
    public function load($file,$context)
    {
        if(!($fr=@fopen($file,'rb')))
        throw new CException(Yii::t('yii','Unable to read file "{file}".',
        array('{file}'=>$file)));

        if(!@flock($fr,LOCK_SH))
        throw new CException(Yii::t('yii','Unable to lock file "{file}" for reading.',
        array('{file}'=>$file)));

        $magic=current($array=unpack('c',$this->readByte($fr,4)));
        if($magic==-34)
        $this->useBigEndian=false;
        else if($magic==-107)
        $this->useBigEndian=true;
        else
        throw new CException(Yii::t('yii','Invalid MO file: {file} (magic: {magic}).',
        array('{file}'=>$file,'{magic}'=>$magic)));

        if(($revision=$this->readInteger($fr))!=0)
        throw new CException(Yii::t('yii','Invalid MO file revision: {revision}.',
        array('{revision}'=>$revision)));

        $count=$this->readInteger($fr);
        $sourceOffset=$this->readInteger($fr);
        $targetOffset=$this->readInteger($fr);

        $sourceLengths=array();
        $sourceOffsets=array();
        fseek($fr,$sourceOffset);
        for($i=0;$i<$count;++$i)
        {
            $sourceLengths[]=$this->readInteger($fr);
            $sourceOffsets[]=$this->readInteger($fr);
        }

        $targetLengths=array();
        $targetOffsets=array();
        fseek($fr,$targetOffset);
        for($i=0;$i<$count;++$i)
        {
            $targetLengths[]=$this->readInteger($fr);
            $targetOffsets[]=$this->readInteger($fr);
        }

        $messages=array();
        for($i=0;$i<$count;++$i)
        {
            $id=$this->readString($fr,$sourceLengths[$i],$sourceOffsets[$i]);
            if(($pos=strpos($id,chr(4)))!==false && substr($id,0,$pos)===$context)
            {
                $id=substr($id,$pos+1);
                $message=$this->readString($fr,$targetLengths[$i],$targetOffsets[$i]);
                $messages[$id]=$message;
            }
        }

        @flock($fr,LOCK_UN);
        @fclose($fr);

        return $messages;
    }

    /**
     * Saves messages to an MO file.
     * @param string $file file path
     * @param array $messages message translations (message id => translated message).
     * Note if the message has a context, the message id must be prefixed with
     * the context with chr(4) as the separator.
     */
    public function save($file,$messages)
    {
        if(!($fw=@fopen($file,'wb')))
        throw new CException(Yii::t('yii','Unable to write file "{file}".',
        array('{file}'=>$file)));

        if(!@flock($fw,LOCK_EX))
        throw new CException(Yii::t('yii','Unable to lock file "{file}" for writing.',
        array('{file}'=>$file)));

        // magic
        if($this->useBigEndian)
        $this->writeByte($fw,pack('c*', 0x95, 0x04, 0x12, 0xde));
        else
        $this->writeByte($fw,pack('c*', 0xde, 0x12, 0x04, 0x95));

        // revision
        $this->writeInteger($fw,0);

        // message count
        $n=count($messages);
        $this->writeInteger($fw,$n);

        // offset of source message table
        $offset=28;
        $this->writeInteger($fw,$offset);
        $offset+=($n*8);
        $this->writeInteger($fw,$offset);
        // hashtable size, omitted
        $this->writeInteger($fw,0);
        $offset+=($n*8);
        $this->writeInteger($fw,$offset);

        // length and offsets for source messagess
        foreach(array_keys($messages) as $id)
        {
            $len=strlen($id);
            $this->writeInteger($fw,$len);
            $this->writeInteger($fw,$offset);
            $offset+=$len+1;
        }

        // length and offsets for target messagess
        foreach($messages as $message)
        {
            $len=strlen($message);
            $this->writeInteger($fw,$len);
            $this->writeInteger($fw,$offset);
            $offset+=$len+1;
        }

        // source messages
        foreach(array_keys($messages) as $id)
        $this->writeString($fw,$id);

        // target messages
        foreach($messages as $message)
        $this->writeString($fw,$message);

        @flock($fw,LOCK_UN);
        @fclose($fw);
    }

    /**
     * Reads one or several bytes.
     * @param resource $fr file handle
     * @param integer $n number of bytes to read
     * @return string bytes
     */
    protected function readByte($fr,$n=1)
    {
        if($n>0)
        return fread($fr,$n);
    }

    /**
     * Writes bytes.
     * @param resource $fw file handle
     * @param string $data the data
     * @return integer how many bytes are written
     */
    protected function writeByte($fw,$data)
    {
        return fwrite($fw,$data);
    }

    /**
     * Reads a 4-byte integer.
     * @param resource $fr file handle
     * @return integer the result
     * @see useBigEndian
     */
    protected function readInteger($fr)
    {
        return current($array=unpack($this->useBigEndian ? 'N' : 'V', $this->readByte($fr,4)));
    }

    /**
     * Writes a 4-byte integer.
     * @param resource $fw file handle
     * @param integer $data the data
     * @return integer how many bytes are written
     */
    protected function writeInteger($fw,$data)
    {
        return $this->writeByte($fw,pack($this->useBigEndian ? 'N' : 'V', (int)$data));
    }

    /**
     * Reads a string.
     * @param resource $fr file handle
     * @param integer $length string length
     * @param integer $offset offset of the string in the file. If null, it reads from the current position.
     * @return string the result
     */
    protected function readString($fr,$length,$offset=null)
    {
        if($offset!==null)
        fseek($fr,$offset);
        return $this->readByte($fr,$length);
    }

    /**
     * Writes a string.
     * @param resource $fw file handle
     * @param string $data the string
     * @return integer how many bytes are written
     */
    protected function writeString($fw,$data)
    {
        return $this->writeByte($fw,$data."\0");
    }
}
